---
id: data-validation
title: 7. 数据校验
sidebar_label: 7. 数据校验
---

import useBaseUrl from "@docusaurus/useBaseUrl";
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

## 7.1 关于数据校验

数据校验字面上的意思就是对使用者提交过来的数据进行合法性验证。在一套完善的应用系统中，数据有效性校验是必不可少的业务处理第一道关卡。

## 7.2 数据校验的好处

- 过滤不安全数据，提高系统的安全性
- 减少不必要的业务异常处理，提高系统的响应速度
- 大大提高系统稳定性
- 大数据并发时起着一定的缓冲作用

## 7.3 数据校验方式

- 传统方式，在业务代码之前手动验证
- `Mvc` 特性方式，`Mvc` 内置的 `DataAnnotations` 方式
- **推荐方式**，`Fur` 框架内置的 `DataValidation` 验证
- 其他方式，使用第三方验证库，如 `FluentValidation`

### 7.3.1 传统方式

在很多老项目中，我们经常看到这样的代码：

```cs {4,9,14}
public bool Insert(Person person)
{
    // 验证参数
    if(string.IsNullOrEmty(person.Name))
    {
        throw new System.Exception("名字不能为空");
    }

    if(person.Age < 18)
    {
        throw new System.Exception("年龄不能小于 18 岁");
    }

    if(!person.Password.Equals(person.ConfirmPassword)
    {
        throw new System.Exception("两次密码不一致");
    }

    // 业务代码
    _repository.Insert(person.Adapt<PersonEntity>());

    // ...
}
```

从上面的代码看起来，似乎没有什么不妥，但是从一个程序可维护性来说，这是一个糟糕的代码，因为该业务代码中**包含了太多与业务无关的数据验证**。

试想一下，如果这个 `Person` 有 几十个参数都需要验证呢？可想而知，这是一个庞大的业务代码。

再者，如果其他地方也需要用到这个 `Person` 类验证呢？那代码好比老鼠啃过的面包屑一样，到处都是。

如此得知，这样的方式是极其不推荐的，**不但污染了业务代码，也破坏了业务职责单一性原理，也让验证逻辑无法实现通用，后续维护难度大大升级**。

### 7.3.2 `Mvc` 特性方式

在 `ASP.NET Core` 中，微软为我们提供了全新的 `特性` 验证方式，可通过对对象贴特性实现数据验证。这种方式有效的将数据校验和业务代码剥离开来，而且容易使用和拓展。

- **在模型中验证**

```cs {1,7-8,11-12}
using System.ComponentModel.DataAnnotations;

namespace Hoa.Application.Authorization.Dtos
{
    public class SignInInput
    {
        [Required]  // 必填验证
        [MinLength(4)]  // 最小长度验证
        public string Account { get; set; }

        [Required]    // 必填验证
        [MaxLength(32)]    // 最大长度验证
        public string Password { get; set; }
    }
}
```

- **在参数中验证**

```cs {2-3,8-9,12-13}
public void CheckMethodParameterValid(
    [Required]    // 必填验证
    [MinLength(4)]    // 最小长度验证
    string name,

    int age,

    [Required]    // 必填验证
    [RegularExpression("[a-zA-Z0-9_]{8,30}")    // 正则表达式验证
    string password,

    [Required]    // 必填验证
    [RegularExpression("[a-zA-Z0-9_]{8,30}")    // 正则表达式验证
    string confirmPassword
)
{
    // TODO
}
```

:::note 小提醒

如果函数的参数大于或等于 3 个，建议抽离出模型类，也就是不建议上面的方式。

:::

- **自定义特性验证**

```cs {1,13-24}
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
    {
        Year = year;
    }

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}
```

- **`IValidatableObject` 复杂验证**

```cs {1,3,10-19}
using System.Collections.Generic;

public class DtoModel : IValidatableObject
{
    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    // 你的验证逻辑
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (你的逻辑代码)
        {
            yield return new ValidationResult(
                "错误消息"
                ,new[] { nameof(Title) }  // 验证失败的属性
            );
        }
    }
}
```

`Mvc` 特性方式极大的将业务逻辑和验证进行了剥离和解耦，而且还能实现自定义复杂验证。

**但是 `Mvc` 特性验证方式有几个明显的缺点**：

- 只能在 `控制器` 中的 `Action`（动作方法）中使用
- **无法在任意类、任意方法中使用**
- 内置的验证类型非常有效，且不易拓展
- 不支持验证消息后期配置

所以，`Fur` 提供了新的验证引擎 `DataValidation`，在完全兼容 `Mvc` 内置验证的同时提供了大量常见验证、复杂验证、自定义验证等能力。

## 7.4 `DataValidation` 验证 🤗

`DataValidation` 是 `Fur` 框架提供了全新的验证方式，完全兼容 `Mvc` 内置验证，并且赋予了超能。

### 7.4.1 `DataValidation` 优点

- **完全兼容 `Mvc` 内置验证引擎**
- **内置常见验证类型及可自定义验证类型功能**
- 提供全局对象拓展验证方式
- 支持验证消息后期配置，支持实时更新
- 支持在任何类，任何方法、任何位置实现手动验证、特性方式验证等
- 支持设置验证结果模型

## 7.5 `DataValidation` 使用

### 7.5.1 注册验证服务

```cs {3,15} title="Fur.Web.Entry/Startup.cs"
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Fur.Web.Entry
{
    public class Startup
    {
        // Other Codes...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApp();
            services.AddControllers()
                    .AddDataValidation();    // 可通过 .AddDataValidation(false) 关闭全局验证处理
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Other Codes...
        }
    }
}
```

:::important 特别注意

`.AddDataValidation()` 需在 `services.AddControllers()` 之后注册。同时 `services.AddApp()` 总是最先注册。

:::

### 7.5.2 兼容 `Mvc` 特性验证

<Tabs
  defaultValue="testdto"
  values={[
    { label: "TestDto", value: "testdto" },
    { label: "FurAppService", value: "furAppService" },
  ]}
>
  <TabItem value="testdto">


```cs {1,7,10}
using System.ComponentModel.DataAnnotations;

namespace Fur.Application
{
    public class TestDto
    {
        [Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")]
        public int Id { get; set; }

        [Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")]
        public string Name { get; set; }
    }
}
```

  </TabItem>
  <TabItem value="furAppService">


```cs {12,22}
using Fur.DynamicApiController;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        /// <summary>
        /// 值类型验证
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public int Get(int id)
        {
            return id;
        }

        /// <summary>
        /// 对象类型验证
        /// </summary>
        /// <param name="testDto"></param>
        /// <returns></returns>
        public TestDto Post(TestDto testDto)
        {
            return testDto;
        }
    }
}
```

  </TabItem>
</Tabs>


如下图所示：

<img src={useBaseUrl("img/sjyz1.gif")} />

### 7.5.3 兼容 `Mvc` 复杂验证

```cs {2,6,14-22}
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Fur.Application
{
    public class TestDto : IValidatableObject
    {
        [Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")]
        public int Id { get; set; }

        [Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")]
        public string Name { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Name.StartsWith("Fur"))
            {
                yield return new ValidationResult(
                    "不能以 Fur 开头"
                    , new[] { nameof(Name) }
                );
            }
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/sjyz2.gif")} />

## 7.6 手动验证

### 7.7.1 验证模型

```cs {1,11}
using Fur.DataValidation;
using Fur.DynamicApiController;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        [NonValidation] // 跳过全局验证
        public DataValidationResult Post(TestDto testDto)
        {
            return testDto.TryValidate();
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/sjyz3.gif")} />

:::note

支持 **`Mvc`** 内置的特性验证、属性验证及复杂的 **`IValidatableObject`** 验证。

:::

### 7.7.2 `ValidationTypes` 常见验证

`Fur` 内置了很多常用类型的数据验证，包括：

- `Required`：非空、非 Null 类型
- `Numeric`：数值类型
- `PositiveNumber`：正数类型
- `NegativeNumber`：负数类型
- `Integer`：整数类型
- `Money`：金钱类型
- `Date`：日期类型
- `Time`：时间类型
- `IDCard`：身份证类型
- `PostCode`：邮编类型
- `PhoneNumber`：手机号类型
- `Telephone`：固话类型
- `PhoneOrTelNumber`：手机或固话类型
- `EmailAddress`：邮件地址类型
- `Url`：网址类型
- `Color`：颜色值类型
- `Chinese`：中文类型
- `IPv4`：IPv4 地址类型
- `IPv6`：IPv6 地址类型
- `Age`：年龄类型
- `ChineseName`：中文名类型
- `EnglishName`：英文名类型
- `Capital`：纯大写英文类型
- `Lowercase`：纯小写英文类型
- `Ascii`：Ascii 类型
- `Md5`：Md5 字符串类型
- `Zip`：压缩包格式类型
- `Image`：图片格式类型
- `Document`：文档格式类型
- `MP3`：Mp3 格式类型
- `Flash`：Flash 格式类型
- `Video`：视频文件格式类型

**使用示例**

<Tabs
  defaultValue="dglx"
  values={[
    { label: "单个类型验证", value: "dglx" },
    { label: "多个组合类型验证", value: "zhlx" },
  ]}
>
<TabItem value="dglx">


```cs
// 验证必填
"".TryValidate(ValidationTypes.Required);   // => false

// 验证中文
"我叫 MonK".TryValidate(ValidationTypes.Chinese); // => false

// 验证数值
2.TryValidate(ValidationTypes.Numeric); // => true

// 验证整数
true.TryValidate(ValidationTypes.Integer); // => false

// 验证邮箱
"monksoul@outlook.com".TryValidate(ValidationTypes.EmailAddress); // => true

// 验证负数
2.0m.TryValidate(ValidationTypes.NegativeNumber); // => false

// 自定义正则表达式验证
"Fur".TryValidate("/^Fur$"); // => true
```

</TabItem>
<TabItem value="zhlx">


```cs
// 验证数值类型且是整数
"20".TryValidate(ValidationTypes.Numeric, ValidationTypes.Integer); // => true

// 验证时日期或时间格式
"2020-05-20".TryValidate(ValidationOptions.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true
"23:45:20".TryValidate(ValidationOptions.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true

```

</TabItem>
</Tabs>


:::tip 小知识

可通过设置 `TryValidate([ValidationOptions], params object[] validationTypes)` 方法的 `ValidationOptions` 参数配置验证逻辑，如：**`同时成立`** 或 **`只要一个成立`** 即可验证通过

:::

### 7.7.3 `[DataValidation]` 特性

`Fur` 还提供了 `[DataValidation]` 特性方便在模型参数中使用 `ValidationTypes` 常见验证或自定义验证。

```cs {1,7,10,14,17,20}
using Fur.DataValidation;

namespace Fur.Application
{
    public class TestDto
    {
        [DataValidation(ValidationTypes.Integer)]
        public int Id { get; set; }

        [DataValidation(ValidationTypes.Numeric, ValidationTypes.Integer)]
        public int Cost { get; set; }

        [DataValidation(ValidationOptions.AtLeastOne, ValidationTypes.Chinese, ValidationTypes.Date)]
        public string Name { get; set; }

        // 可以和Mvc特性共存
        [Required, DataValidation(ValidationTypes.Age)]
        public int Age { get; set; }

        [DataValidation(ValidationTypes.IDCard, ErrorMessage = "自定义身份证提示消息")]
        public string IDCard { get; set; }
    }
}
```

## 7.7 `[NonValidation]` 跳过验证

`Fur` 框架提供了对象模型跳过验证特性 `[NonValidation]`，支持在 `控制器`，`动作方法`，`类` 中使用。

一旦贴了此特性，那么将不会执行验证操作。

:::note

**`[NonValidation]`** 只对对象类型有效，值类型无效。

:::

## 7.8 高级自定义操作

### 7.8.1 自定义 `ValidationTypes` 类型

除了 `Fur` 内置的验证类型以外，`Fur` 还提供了非常灵活的自定义验证类型机制。

实现自定义验证类型必须遵循以下配置：

- 验证类型必须时公开且时 `Enum` 枚举类型
- 枚举类型必须贴有 `[ValidationType]` 特性
- 枚举中每一项必须贴有 `[ValidationItemMetadata]` 特性

**如**：

```cs {1,6,12,18}
using Fur.DataValidation;
using System.Text.RegularExpressions;

namespace Fur.Application
{
    [ValidationType]
    public enum MyValidationTypes
    {
        /// <summary>
        /// 强密码类型
        /// </summary>
        [ValidationItemMetadata(@"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$", "必须须包含大小写字母和数字的组合，不能使用特殊字符，长度在8-10之间")]
        StrongPassword,

        /// <summary>
        /// 以 Fur 字符串开头，忽略大小写
        /// </summary>
        [ValidationItemMetadata(@"^(fur).*", "默认提示：必须以Fur字符串开头，忽略大小写", RegexOptions.IgnoreCase)]
        StartWithFurString
    }
}

```

**使用**

- 手动使用

```cs
"q1w2e3".TryValidate(MyValidationTypes.StrongPassword); // => false

"furos".TryValidate(MyValidationTypes.StartWithFurString); // => true
```

- `[DataValidation]` 中使用

```cs
[DataValidation(MyValidationTypes.StrongPassword)]
public string Password { get; set; }
```

- 多个自定义类型混用

```cs
"Q1w2e3r4t5!*".TryValidate(MyValidationTypes.StrongPassword, ValidationTypes.Required); // => true
```

:::caution 特别注意

自定义的验证类型也要保证名称全局唯一，也就是多个验证类型不能出现一样的名字。

:::

### 7.8.2 自定义 `ValidationTypes` 失败消息

`Fur` 内置的 `ValidationTypes` 已有默认的失败消息：

- `Required`：**The Value is required.**
- `Numeric`：**The Value is not a numeric type.**
- `PositiveNumber`：**The Value is not a positive number type.**
- `NegativeNumber`：**The Value is not a negative number type.**
- `Integer`：**The Value is not a integer type.**
- `Money`：**The Value is not a money type.**
- `Date`：**The Value is not a date type.**
- `Time`：**The Value is not a time type.**
- `IDCard`：**The Value is not a idcard type.**
- `PostCode`：**The Value is not a postcode type.**
- `PhoneNumber`：**The Value is not a phone number type.**
- `Telephone`：**The Value is not a telephone type.**
- `PhoneOrTelNumber`：**The Value is not a phone number or telephone type.**
- `EmailAddress`：**The Value is not a email address type.**
- `Url`：**The Value is not a url address type.**
- `Color`：**The Value is not a color type.**
- `Chinese`：**The Value is not a chinese type.**
- `IPv4`：**The Value is not a IPv4 type.**
- `IPv6`：**The Value is not a IPv6 type.**
- `Age`：**The Value is not a age type.**
- `ChineseName`：**The Value is not a chinese name type.**
- `EnglishName`：**The Value is not a english name type.**
- `Capital`：**The Value is not a capital type.**
- `Lowercase`：**The Value is not a lowercase type.**
- `Ascii`：**The Value is not a ascii type.**
- `Md5`：**The Value is not a md5 type.**
- `Zip`：**The Value is not a zip type.**
- `Image`：**The Value is not a image type.**
- `Document`：**The Value is not a document type.**
- `MP3`：**The Value is not a mp3 type.**
- `Flash`：**The Value is not a flash type.**
- `Video`：**The Value is not a video type.**

我们可以通过创建继承 `IValidationMessageTypeProvider` 验证消息提供器类型，或通过 `appsettings.json` 配置。

- **`[ValidationMessageType]`** 方式

```cs {5,9,12,15,19,22}
using Fur.DataValidation;

namespace Fur.Application
{
    [ValidationMessageType]
    public enum MyValidationMessageType
    {
        // 修改内置验证类型验证失败消息
        [ValidationMessage("值不能为空或Null")]
        Required,

        [ValidationMessage("必须是数值类型")]
        Numeric,

        [ValidationMessage("必须是正数")]
        PositiveNumber,

        // 修改自定义类型验证失败消息
        [ValidationMessage("密码太简单了")]
        StrongPassword,

        [ValidationMessage("必须以 Fur 开头")]
        StartWithFurString
    }
}
```

:::tip 小知识

除了贴 `[ValidationMessageType]` 特性外，`Fur` 框架还提供了 `IValidationMessageTypeProvider` 方式查找验证消息类型，如下图所示：

```cs {1,6,8-12}
using Fur.DataValidation;
using System;

namespace Fur.Application
{
    public class MyValidationTypeMessageProvider : IValidationMessageTypeProvider
    {
        public Type[] Definitions => new[]
        {
            typeof(MyValidationMessageType),
            typeof(MyValidationMessageType2)
        };
    }
}
```

注册验证消息提供器

```cs {3,15} title="Fur.Web.Entry/Startup.cs"
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Fur.Web.Entry
{
    public class Startup
    {
        // Other Codes...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApp();
            services.AddControllers()
                    .AddDataValidation<MyValidationTypeMessageProvider>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Other Codes...
        }
    }
}
```

:::

如下图所示：

<img src={useBaseUrl("img/sjyz4.png")} />

- **`appsettings.json` 方式**

```json {2-11} title="Fur.Web.Entry/appsettings.json"
{
  "AppSettings": {
    "ValidationTypeMessageSettings": {
      "Definitions": [
        ["Required", "值不能为空或Null"],
        ["Numeric", "必须是数值类型"],

        ["StrongPassword", "密码太简单了!!!"]
      ]
    }
  }
}
```

:::important

`appsettings.json` 中相同的 `Key` 或覆盖 `IValidationMessageTypeProvider` 提供相同 `Key` 的值。

:::

#### 错误消息查找优先级

`DefaultErrorMessage` -> `IValidationMessageTypeProvider` -> `appsettings.json` (**低 -> 高**)

## 7.9 模型验证范围

`Fur` 提供多种模型验证范围设置：

- 全局验证（默认）
- `[NonValidation]` 跳过验证
- `[TypeFilter(typeof(DataValidationFilter))]` 局部验证
- `[ApiController]` 控制器范围验证

### 7.9.1 全局验证

默认情况下，通过 `.AddDataValidation()` 注册数据验证服务已经启用了全局验证，如若不想启用全局验证，则传入 `false` 即可，如：`.AddDataValidation(false)`。

### 7.9.2 `[NonValidation]` 跳过验证

可通过 `[NonValidation]` 贴在 `控制器`，`动作方法`，`类` 中跳过全局验证或不需要验证

### 7.9.3 `[TypeFilter(typeof(DataValidationFilter))]` 局部验证

我们也可以无需注册 `.AddDataValidation()` 服务，直接在 `动作方法` 上贴 `[TypeFilter(typeof(DataValidationFilter))]` 可启用局部验证。如：

```cs {1,3,9}
using Fur.DataValidation;
using Fur.DynamicApiController;
using Microsoft.AspNetCore.Mvc;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        [TypeFilter(typeof(DataValidationFilter))]
        public TestDto Post(TestDto testDto)
        {
            return testDto;
        }
    }
}
```

### 7.9.4 `[ApiController]` 控制器范围验证

`[ApiController]` 是 `Mvc` 提供的控制器范围（含所有动作方法）的验证。

```cs {1,5}
using Microsoft.AspNetCore.Mvc;

namespace Fur.Web.Entry.Controllers
{
    [ApiController]
    public class MvcController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}
```

## 7.10 `MiniProfiler` 查看

如下图所示：

<img src={useBaseUrl("img/sjyz5.png")} />

## 7.10 多语言支持

文档整理中...

## 7.11 验证模型提供器

文档整理中...

## 7.12 反馈与建议

:::note 与我们交流

给 Fur 提 [Issue](https://gitee.com/monksoul/Fur/issues/new?issue)。

:::
